﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using DynORM.Interfaces;
using DynORM.ValueObjects;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Xml;
using DynORM.TypeMappingSubsystem;

namespace DynORM.ReferenceObjects
{

    [XmlRoot(ElementName = Row.RowTag)]
    public class Row : IRow, IXmlSerializable
    {
        #region fields
        private List<IField> fields = new List<IField>();
        private List<IForeignKey> foreignKeys = new List<IForeignKey>();
        //it's not allowed to change primary key field values. they identify the row. if need to do that create a new row.
        private IPrimaryKey primaryKey;  //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        #endregion

        #region constructors
        public Row(IRowDescriptor rowDescriptor, params object[] values)
        {
            int g = 0;
            foreach (IRelationDescriptor relationDescriptor in rowDescriptor.RelationDescriptors.OrderBy<IRelationDescriptor,string>(rd=>rd.Name))
            {
                object[] fetchedValues;
                if (relationDescriptor.RelationType == RelationType.ForeignKey)
                {
                    int count = relationDescriptor.FieldDescriptors.Count() * 2; //for ForeignKeyDescriptor we store values for Fields and RelatedFields in this array so we double it in size
                    fetchedValues = new object[count];
                    for (int i = 0; i < count; i++)
                        fetchedValues[i] = values[g++];
                    this.foreignKeys.Add(new ForeignKey((ForeignKeyDescriptor)relationDescriptor, fetchedValues));
                }
                else if (relationDescriptor.RelationType == RelationType.PrimaryKey)
                {
                    int count = relationDescriptor.FieldDescriptors.Count();
                    fetchedValues = new object[count];
                    for (int i = 0; i < count; i++)
                        fetchedValues[i] = values[g++];
                    this.primaryKey = new PrimaryKey((PrimaryKeyDescriptor)relationDescriptor, fetchedValues);
                }
                else
                    throw new Exception("Relation type not supported.");
            }
            foreach (IFieldDescriptor fieldDescriptor in rowDescriptor.FieldDescriptors.OrderBy<IFieldDescriptor,string>(fd=> fd.FieldName))
                this.fields.Add(new Field(fieldDescriptor, values[g++]));
        }

        //create a new row with empty default values in its fields 
        //with structure defined with IRowDescriptor and row identified with IPrimaryKey
        public Row(IPrimaryKey pk, IRowDescriptor rowDescriptor)
        {
            this.primaryKey = pk;

            foreach (IRelationDescriptor relationDescriptor in rowDescriptor.RelationDescriptors)
                if (relationDescriptor.RelationType == RelationType.ForeignKey)
                {
                    List<object> defaultvalues = new List<object>();
                    List<object> reldefaultvalues = new List<object>();
                    foreach (KeyValuePair<int, IFieldDescriptor> fd in relationDescriptor.FieldDescriptors)
                    {
                        defaultvalues.Add(Types.GetDefaultValue(fd.Value.FieldType));
                        reldefaultvalues.Add(Types.GetDefaultValue(fd.Value.FieldType));
                    }
                    this.foreignKeys.Add(new ForeignKey((ForeignKeyDescriptor)relationDescriptor, defaultvalues.Union(reldefaultvalues)));
                }

            foreach (IFieldDescriptor fieldDescriptor in rowDescriptor.FieldDescriptors)
                this.fields.Add(new Field(fieldDescriptor, Types.GetDefaultValue(fieldDescriptor.FieldType)));
        }

        public Row(IEnumerable<IField> fields, IEnumerable<IRelation> relations)
        {
            if (relations.Count<IRelation>(r => r.RelationType == RelationType.PrimaryKey) != 1)
                throw new Exception("Cannot create row which contains more then one primary key constraint.");
            
            foreach (IRelation relation in relations)
                if (relation.RelationType == RelationType.PrimaryKey)
                    this.primaryKey = (IPrimaryKey)relation;
                else
                    this.foreignKeys.Add((IForeignKey)relation);
            foreach (IField field in fields)
                this.fields.Add(field);
        }

        public Row(IPrimaryKey pk, IEnumerable<IField> fields, IEnumerable<IForeignKey> fks)
        {
            this.primaryKey = pk;
            foreach (IForeignKey fk in fks)
                    this.foreignKeys.Add(fk);
            foreach (IField field in fields)
                this.fields.Add(field);
        }

        //for serializer
        private Row()
        { }
        #endregion

        public const string RowTag = "Row";
        public const string RowTableNameAttr = "TableName";
        public const string RowForeignKeysTag = "ForeignKeys";
        public const string RowFieldsTag = "Fields";


        public string TableName
        {
            get
            {
                return this.primaryKey.TableName;
            }
        }

        //uniquely identify Row. Does not change during row's lifetime
        public IPrimaryKey PrimaryKey
        { 
            get
            {
                return this.primaryKey;
            }
        }
        
        //used to simplify field's value assignment
        //returns IField.FieldValue. fields that are in IEnumerable<IRelation> Relations can be get by means of this indexer too
        //it's not allowed to change primary key field values. they identify the row. if need to do that create a new row.
        public object this[string fieldName] 
        {
            get 
            {
                IField foundField = null;
                foreach (IForeignKey fk in this.foreignKeys)
                {
                    foundField = fk.Fields.FirstOrDefault<KeyValuePair<int, IField>>(kv => String.Compare(kv.Value.FieldName, fieldName, StringComparison.OrdinalIgnoreCase) == 0).Value;
                    if (foundField != null)
                        return foundField.FieldValue;
                }
                foundField = this.fields.Find(match => String.Compare(match.FieldName, fieldName, StringComparison.OrdinalIgnoreCase) == 0);
                if (foundField != null)
                    return foundField.FieldValue;
                throw new Exception(String.Format("Field {0} was not fould.", fieldName));
            }
            set 
            {
                IField foundField = null;
                bool foundInRelations = false;
                IForeignKey newFK = null;
                List<IForeignKey> newForeignKeys = new List<IForeignKey>();
                foreach (IForeignKey fk in this.foreignKeys)
                {
                    foundField = fk.Fields.FirstOrDefault<KeyValuePair<int, IField>>(kv => String.Compare(kv.Value.FieldName, fieldName, StringComparison.OrdinalIgnoreCase) == 0).Value;
                    if (foundField != null)
                    {
                        newFK = (IForeignKey)AuxRelation.ChangeFieldInRelation(fk, (FieldDescriptor)foundField.GetDescriptor(), value);
                        newForeignKeys.Add(newFK);
                        foundInRelations = true;
                    }
                    else
                    {
                        newForeignKeys.Add(fk);
                    }
                }
                if (foundInRelations)
                {
                    this.foreignKeys = newForeignKeys;
                    return;
                }

                foundField = this.fields.Find(match => String.Compare(match.FieldName, fieldName, StringComparison.OrdinalIgnoreCase) == 0);
                if (foundField != null)
                {
                    this.fields.Remove(foundField);
                    IField newF = new Field((FieldDescriptor)foundField.GetDescriptor(), value);
                    this.fields.Add(newF);
                    return;
                }
                throw new Exception(String.Format("Field {0} was not fould.", fieldName));
            }
        }

        public IEnumerable<IField> Fields
        {
            get
            {
                return this.fields;
            }
        }

        //relations retrieving
        public IEnumerable<IForeignKey> ForeignKeys
        {
            get
            {
                return this.foreignKeys;
            }
        }

        public IEnumerable<IRelation> Relations
        {
            get
            {
                List<IRelation> relations = new List<IRelation>();
                relations.AddRange(this.foreignKeys);
                relations.Add(this.primaryKey);
                return relations;
            }
        }

        public CheckIntegrityResult CheckValuesIntegrity(bool usePredefinedConvention)
        {
            CheckIntegrityResult passed = new CheckIntegrityResult(true, String.Empty);

            //just because we construct Rows not only from RowDescriptors we have to recheck structure integrity before value integrity
            CheckIntegrityResult checkStructureIntegrityRes = this.GetDescriptor().CheckStructureIntegrity(false);
            if (checkStructureIntegrityRes != passed)
                return checkStructureIntegrityRes;

            foreach (IField f in this.fields)
            {
                if (f.CheckValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "A field contained in the row failed its integrity check. See inner CheckIntegrityResult.", f.CheckValuesIntegrity(false));
            }

            foreach (IRelation r in this.Relations)
            {
                if (r.CheckValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "A relation contained in the row failed its integrity check. See inner CheckIntegrityResult.", r.CheckValuesIntegrity(false));
            }

            return passed;
        }

        public CheckIntegrityResult CheckOnlyValuesIntegrity(bool usePredefinedConvention)
        {
            //this method is used in case when several rows from the same table are retrieved. in this case structure (RowDescriptor) is checked only once and values (Rows) check do not include structure tests
            CheckIntegrityResult passed = new CheckIntegrityResult(true, String.Empty);

            foreach (IField f in this.fields)
            {
                if (f.CheckOnlyValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "A field contained in the row failed its integrity check. See inner CheckIntegrityResult.", f.CheckValuesIntegrity(false));
            }

            foreach (IRelation r in this.Relations)
            {
                if (r.CheckOnlyValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "A relation contained in the row failed its integrity check. See inner CheckIntegrityResult.", r.CheckValuesIntegrity(false));
            }

            return passed;
        }

        public IORMObjectDescriptor GetDescriptor()
        {
            List<IFieldDescriptor> fieldDescriptors = new List<IFieldDescriptor>();
            List<IRelationDescriptor> relationDescriptors = new List<IRelationDescriptor>();
            foreach (IField f in this.fields)
                fieldDescriptors.Add((IFieldDescriptor)f.GetDescriptor());
            foreach (IRelation r in this.Relations)
                relationDescriptors.Add((IRelationDescriptor)r.GetDescriptor());
            return new RowDescriptor(this.TableName, fieldDescriptors, relationDescriptors);
        }

        #region IXmlSerializable and auxiliary CreateFromXml member
        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader)
        {
            do
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        {
                            if (string.Compare(reader.Name, RowTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                bool emptyElement = reader.IsEmptyElement;
                                if (emptyElement)
                                    return;
                            }
                            else if (string.Compare(reader.Name, DynORM.ValueObjects.PrimaryKey.PrimaryKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                PrimaryKey pk = DynORM.ValueObjects.PrimaryKey.CreateFromXml(reader);
                                this.primaryKey = pk;
                            }
                            else if (string.Compare(reader.Name, RowForeignKeysTag, StringComparison.OrdinalIgnoreCase) == 0)
                            { }
                            else if (string.Compare(reader.Name, ForeignKey.ForeignKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                ForeignKey fk = ForeignKey.CreateFromXml(reader);
                                this.foreignKeys.Add(fk);
                            }
                            else if (string.Compare(reader.Name, RowFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            { }
                            else if (string.Compare(reader.Name, Field.FieldTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                Field f = Field.CreateFromXml(reader);
                                this.fields.Add(f);
                            }
                        } break;
                    case XmlNodeType.EndElement:
                        {
                            if (string.Compare(reader.Name, RowTag, StringComparison.OrdinalIgnoreCase) == 0)
                                return;
                        } break;
                    default: continue;
                }
            } while (reader.Read());
        }

        public static Row CreateFromXml(XmlReader reader)
        {
            IPrimaryKey primaryKey = null;
            List<IForeignKey> foreignKeys = new List<IForeignKey>();
            List<IField> fields = new List<IField>();

            do
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        {
                            if (string.Compare(reader.Name, RowTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                bool emptyElement = reader.IsEmptyElement;
                                if (emptyElement)
                                    return new Row(primaryKey, fields, foreignKeys);
                            }
                            else if (string.Compare(reader.Name, DynORM.ValueObjects.PrimaryKey.PrimaryKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                PrimaryKey pk = DynORM.ValueObjects.PrimaryKey.CreateFromXml(reader);
                                primaryKey = pk;
                            }
                            else if (string.Compare(reader.Name, RowForeignKeysTag, StringComparison.OrdinalIgnoreCase) == 0)
                            { }
                            else if (string.Compare(reader.Name, ForeignKey.ForeignKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                ForeignKey fk = ForeignKey.CreateFromXml(reader);
                                foreignKeys.Add(fk);
                            }
                            else if (string.Compare(reader.Name, RowFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            { }
                            else if (string.Compare(reader.Name, Field.FieldTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                Field f = Field.CreateFromXml(reader);
                                fields.Add(f);
                            }
                        } break;
                    case XmlNodeType.EndElement:
                        {
                            if (string.Compare(reader.Name, RowTag, StringComparison.OrdinalIgnoreCase) == 0)
                                return new Row(primaryKey, fields, foreignKeys);
                        } break;
                    default: continue;
                }
            } while (reader.Read());
            return new Row(primaryKey, fields, foreignKeys);
        }

        public void WriteXml(XmlWriter writer)
        {
            writer.WriteAttributeString(RowTableNameAttr, this.TableName);

            writer.WriteStartElement(DynORM.ValueObjects.PrimaryKey.PrimaryKeyTag);
            ((PrimaryKey)this.primaryKey).WriteXml(writer);
            writer.WriteEndElement();

            if (this.ForeignKeys.Count() > 0)
            {
                writer.WriteStartElement(RowForeignKeysTag);
                foreach (ForeignKey fk in this.ForeignKeys)
                {
                    writer.WriteStartElement(ForeignKey.ForeignKeyTag);
                    fk.WriteXml(writer);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }

            if (this.Fields.Count() > 0)
            {
                writer.WriteStartElement(RowFieldsTag);
                foreach (Field f in this.Fields)
                {
                    writer.WriteStartElement(Field.FieldTag);
                    f.WriteXml(writer);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
        }
        #endregion    

    }

}
